#include <stdlib.h>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <fstream>

#include "mysql_connection.h"
#include "mysql_driver.h"
#include "version.h"

#include <cppconn/driver.h>
#include <cppconn/exception.h>
#include <cppconn/resultset.h>
#include <cppconn/statement.h>
#include <cppconn/prepared_statement.h>

#define EXAMPLE_HOST "localhost"
#define EXAMPLE_USER "root"
#define EXAMPLE_PASS "ase45f66"
#define EXAMPLE_DB "simple"

using namespace std;
using namespace AutoVersion;

struct FieldInfo {
    string fieldName;
    string fieldType;
};

void reformat(string& word)
{
  int wlen;
  wlen = word.length();
  word[0] = toupper(word[0]);
  for (int i=1; i<wlen; i++)
    word[i] = tolower(word[i]);
}

int main(int argc, const char **argv)
{

    string url(argc >= 2 ? argv[1] : EXAMPLE_HOST);
    const string user(argc >= 3 ? argv[2] : EXAMPLE_USER);
    const string pass(argc >= 4 ? argv[3] : EXAMPLE_PASS);
    const string database(argc >= 5 ? argv[4] : EXAMPLE_DB);

    cout << "JBaker v" << FULLVERSION_STRING << endl;
    cout << endl;


    try
    {
        system("mkdir model");

        sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
        sql::Connection *con = driver->connect(url, user, pass);

        con->setSchema(database);

        sql::Statement *stmt = con->createStatement();

        sql::ResultSet *rsTables = stmt->executeQuery("SHOW TABLES");

        // loop through all tables of database
        while (rsTables->next()) {
            string tableName = rsTables->getString(1);
            string className = tableName;
            reformat(className);
            string classFileName = std::string("model/"+className+".java");
            ofstream f;
            f.open(classFileName.c_str());
            f << "package model;" << endl << endl;
            f << "import java.util.Date;" << endl;
            f << "import java.util.List;" << endl << endl;
            f << "public class " << className << " extends Model {" << endl;
            f << endl;
            sql::ResultSet *rsFields = stmt->executeQuery("DESCRIBE "+tableName);
            list<FieldInfo> fields;

            // declare class properties
            while (rsFields->next())
            {
                std::string dbType = rsFields->getString("type").substr(0,rsFields->getString("type").find('('));
                std::string fieldName = rsFields->getString("field");
                std::string fieldType = "";
                // determine the java type base on dbType
                if (dbType=="int")
                    fieldType = "Integer";
                else if (dbType=="bigint")
                    fieldType = "Long";
                else if (dbType=="datetime")
                    fieldType = "Date";
                else if (dbType=="tinyint")
                    fieldType = "Boolean";
                else if (dbType=="varchar")
                    fieldType = "String";
                else if (dbType=="text")
                    fieldType = "String";

                f << "\tprotected " << fieldType << " " << fieldName << " = null;" << endl;
                FieldInfo field = { fieldName, fieldType };
                fields.push_back(field);
            }

            //generate getters and setters for properties
            while (!fields.empty()) {
                FieldInfo fd = fields.front();
                string alterFieldName = fd.fieldName;
                reformat(alterFieldName);
                f << endl << "\tpublic " << fd.fieldType << " get" << alterFieldName << "() {" << endl;
                f << "\t\treturn this." << fd.fieldName << ";" << endl;
                f << "\t}" << endl;
                f << endl << "\tpublic " << fd.fieldType << " set" << alterFieldName << "(" << fd.fieldType << " _" << fd.fieldName << ") {" << endl;
                f << "\t\tthis." << fd.fieldName << " = _" << fd.fieldName << ";" << endl;
                f << "\t}" << endl;
                fields.pop_front();
            }

            //look for 1-M relationship then generate method to get childs
            std::string sqlStr = "select ";
                        sqlStr += "concat(table_name,'.',column_name) as 'foreign_key', ";
                        sqlStr += "concat(referenced_table_name,'.',referenced_column_name) as 'references' ";
                        sqlStr += "from information_schema.key_column_usage ";
                        sqlStr += "where referenced_table_name = '"+tableName+"' ";
                        sqlStr += "and table_schema='"+database+"';";

            sql::ResultSet *rsChilds = stmt->executeQuery(sqlStr);

            while (rsChilds->next()) {
                //get foreign key
                std::string foreignkey = rsChilds->getString("foreign_key");
                foreignkey = foreignkey.substr(foreignkey.find('.')+1,foreignkey.length()-foreignkey.find('.'));

                //find this table primary key
                std::string primakey = rsChilds->getString("references");
                primakey = primakey.substr(primakey.find('.')+1,primakey.length()-primakey.find('.'));
                primakey = "this." + primakey;
                std::string childTableName = rsChilds->getString("foreign_key");
                childTableName = childTableName.substr(0,childTableName.find('.'));
                std::string childClassName = childTableName;
                reformat(childClassName);
                f << endl << "\tpublic " << "List<" << childClassName << ">" << " get" << childClassName << "s() {" << endl;
                f << "\t\treturn new " << childClassName << "().search(\"" << foreignkey << "=\" + " << primakey << ");" << endl;
                f << "\t}" << endl;
            }

            //look for parents relationship
                        sqlStr  = "select ";
                        sqlStr += "concat(table_name,'.',column_name) as 'foreign_key', ";
                        sqlStr += "concat(referenced_table_name,'.',referenced_column_name) as 'references' ";
                        sqlStr += "from information_schema.key_column_usage ";
                        sqlStr += "where referenced_table_name is not null ";
                        sqlStr += "and table_name = '"+ tableName +"' ";
                        sqlStr += "and table_schema='"+database+"';";

            sql::ResultSet *rsParents = stmt->executeQuery(sqlStr);

            while (rsParents->next()) {
                //get parent class
                std::string parentName = rsParents->getString("references");
                parentName = parentName.substr(0,parentName.find('.'));
                reformat(parentName);

                //get foreign key
                std::string foreignkey = rsParents->getString("foreign_key");
                foreignkey = foreignkey.substr(foreignkey.find('.')+1,foreignkey.length()-foreignkey.find('.'));

                f << endl << "\tpublic " << parentName << " get" << parentName << "() {" << endl;
                f << "\t\treturn new " << parentName << "(this." << foreignkey << ");" << endl;
                f << "\t}" << endl;
            }

            f << endl;
            f << "}";

            delete rsFields;
            delete rsChilds;
            delete rsParents;
        }

        delete rsTables;
        delete stmt;
        delete con;
    }
    catch (sql::SQLException &e)
    {
        /*
          The MySQL Connector/C++ throws three different exceptions:

          - sql::MethodNotImplementedException (derived from sql::SQLException)
          - sql::InvalidArgumentException (derived from sql::SQLException)
          - sql::SQLException (derived from std::runtime_error)
        */
        cout << "# ERR: SQLException in " << __FILE__;
        cout << "(" << __FUNCTION__ << ") on line " << __LINE__ << endl;
        /* Use what() (derived from std::runtime_error) to fetch the error message */
        cout << "# ERR: " << e.what();
        cout << " (MySQL error code: " << e.getErrorCode();
        cout << ", SQLState: " << e.getSQLState() << " )" << endl;

        return EXIT_FAILURE;
    }



    cout << "Done." << endl;

    return EXIT_SUCCESS;
}
